Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS
Unfortunately, people seem to treat the $timeout() function in AngularJS as a set-it and forget-it type of function. But, forgetting about your $timeout callback can have negative consequences, all the way from code that fails silently to code that raises an exception to code that makes repeated $http requests to your server for no reason. The trick to managing your $timeout timers properly is to cancel them in your $destroy events.
View this demo in my JavaScript-Demos project on GitHub.
Unlike the core JavaScript setTimeout() and setInterval() functions, the $timeout() function in AngularJS returns a promise. And, just like any other promise, you can bind to the $timeout's resolved and rejected events. More importantly, however, you can cancel the underlying timer by passing the promise off to the $timeout.cancel() method.
In an AngularJS application, this becomes very important because timers can end up executing code that is no longer relevant to the state of the application and the user interface. At best, this happens silently; at worse, this causes unexpected behavior that leads to a poor user experience. To keep things running smoothly, I recommend that you always keep a handle on your $timeout timers; and, that you call the $timeout.cancel() method anytime the containing Controller or Directive receives the $destroy event.
To see this in action, I have a few DOM elements below that are created and destroyed using the ngSwitch/ngSwitchWhen directives. Notice that when the $destroy event is triggered (this case, in our Directive), I am canceling the current timer.
<!doctype html>
<html ng-app="Demo" ng-controller="DemoController">
<head>
<meta charset="utf-8" />
<title>
Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS
</title>
</head>
<body>
<h1>
Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS
</h1>
<p>
<a href="#" ng-click="toggle()">Toggle Section</a>
</p>
<div ng-switch="section">
<p ng-switch-when="happy" bn-directive>
Oh sweet!
</p>
<p ng-switch-when="sad" bn-directive>
Oh noes!
</p>
</div>
<!-- Load jQuery and AngularJS. -->
<script
type="text/javascript"
src="../../vendor/jquery/jquery-2.0.3.min.js">
</script>
<script
type="text/javascript"
src="../../vendor/angularjs/angular-1.0.7.min.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the main demo.
app.controller(
"DemoController",
function( $scope ) {
$scope.section = "happy";
// I toggle the section value, to show/hide the
// differnet sections in the markup.
$scope.toggle = function() {
if ( $scope.section === "happy" ) {
$scope.section = "sad";
} else {
$scope.section = "happy";
}
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I'm just a sample directove to demonstrate the clearing
// of a $timeout event in the AngularJS $destroy event.
app.directive(
"bnDirective",
function( $timeout ) {
// I bind the User Interface events to the $scope.
function link( $scope, element, attributes ) {
// When the timeout is defined, it returns a
// promise object.
var timer = $timeout(
function() {
console.log( "Timeout executed", Date.now() );
},
2000
);
// Let's bind to the resolve/reject handlers of
// the timer promise so that we can make sure our
// cancel approach is actually working.
timer.then(
function() {
console.log( "Timer resolved!", Date.now() );
},
function() {
console.log( "Timer rejected!", Date.now() );
}
);
// When the DOM element is removed from the page,
// AngularJS will trigger the $destroy event on
// the scope. This gives us a chance to cancel any
// pending timer that we may have.
$scope.$on(
"$destroy",
function( event ) {
$timeout.cancel( timer );
}
);
}
// Return the directive configuration.
return({
link: link,
scope: false
});
}
);
</script>
</body>
</html>
Most of the time, letting your $timeout timer run its course probably won't lead to a negative outcome. But, I suggest getting into the practice of clearing $timeout's since they can lead to a poor user experience (or apply unnecessary load to your server). This applies to both Controllers and Directives.
Want to use code from this post? Check out the license.
Reader Comments
Hi,
I came cross this great page, what I was looking for is to verify the $destroy event can be properly fired as expected, I copy you code and add some log to console in the $destroy event, but nothing happens.
$scope.$on(
"$destroy",
function( event ) {
$timeout.cancel( timer );
console.log( "Timer Canceled!", Date.now() );
}
);
Can you explain?
Thanks
John
@John,
I am sorry, I do not understand what you are asking? AngularJS will implicitly trigger the $destroy event when the relevant view is destroyed. In the majority of cases, you won't ever have to trigger $destroy yourself.
The exception to this is when you are creating Directives that implement transclusion. In those cases, you may have to explicitly call $scope.$destroy() - but, again, very unlikely for your day-to-day work.
I really love the way Angular handles scope automatically. I was kind of hoping for an unlink or teardown attribute in the directive though that would set the listener for me.
It goes without saying, you also need to do this for $interval.
Nice post, Thanks!
Hi,
i am trying ng-switch to show a div and hide another on same condition.
in my controller i write the following code
$scope.currentView = 'view1';
$scope.loadView = function (viewName) {
$scope.currentView = 'view2';
};
My problem is after making the $scope.currentview as view2 the first statement is executed and it is again set to view 1 hence not changing the divs.My cshtml is
<div data-ng-switch on="currentView">
<div class="displayContents" data-ng-switch-when="view1">
</div>
</div>
<div class="createContent" data-ng-switch-when="view2">
</div>
please suggest me a solution
Thanks.
Thank you very much.This was of great help to me.
Hey, I just fumbled on that post while looking up something else, and I thought I would take the opportunity to ask this, as it's a question specificity I haven't seen answered directly:
For situations like the one you described here, I'm all good with destroying the $timeout (should I say 'promise'). What I am not exactly clear about is in the event you use $timeout to execute a piece of code on the next digest cycle like in:
$timeout(function() {
//do something later
});
What's your take in that situation?
I know most of the time you can rely on $scope.$evalAsync for a very similar result, but sometimes evalAsync won't just cut it for me.
Thanks!
I wonder how it may be possible to skip subscribing to each $destroy event to cancel each $timeout.
Maybe if you place the $timeout instance in a property inside the scope object, itwould be cancelled as the outcome of the scope being destroyed.
instead of:
var timer = $timeout(someHandler);
do:
$scope.timer = $timeout(someHandler);
Does that make sense?
@Tommy
When you call $timeout.cancel(promise) then angular runs code to cancel the promise passed and do cleanup.
Angulars $scope.$destroy() will make that scope object available for garbage collection (by removing all references to it) but it has no knowledge of what properties are on the scope or how they should be cleaned up.
So no, I don't see how if you place the $timeout instance in a property inside the scope object, it would be cancelled as the outcome of the scope being destroyed.
I think you should explicitly call $timeout.cancel(promise)
Thanks for the thoughtful response.
Since this is so common, I would make it a practice to have all the timers in an array like $scope.timeouts. Then have a generic util function that will get the scope, and will subscribe to the $destroy event to then cancel all timers.
Thanks again!
Hi!
Typo on line 84 `directove`=>`directive`
Follow up on this topic.
How about when $timeout is used in a service?
ds
Great!
but lets admit it, it's ugly and bug prone..
why did angular not come up with `$scope.$timeout()` function that simply does that?
Here is how to add it
```
angular.module('MyApp',[])....run ( function( $rootScope, $timeout ) {
var Scope = Object.getPrototypeOf($rootScope);
Scope.$timeout = function( func, time ){
var timer = $timeout(func,time);
this.$on('$destroy', function(){ $timeout.cancel(timer); };
return timer;
}
});
```
more or less - you get the point..
same for $interval?
and now you have an intuitive api. $scope.$interval, $scope.$timeout - that only exist with scope..
Thanks @Guy,
That's brilliant!
Its a great Article. Thank you.